using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;

namespace OpenWaves
{
    public partial class UrlPath : IEquatable<UrlPath>
    {
        private readonly UrlPathOptions options;

        public static UrlPath Combine(string path1, string path2)
        {
            Contract.Requires<ArgumentNullException>(String.IsNullOrEmpty(path1) == false);
            Contract.Requires<ArgumentNullException>(String.IsNullOrEmpty(path2) == false);
            Contract.Ensures(Contract.Result<UrlPath>() != null);

            var parsedPath1 = Parse(path1);
            var parsedPath2 = Parse(path2);

            return parsedPath1.Combine(parsedPath2);
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "c")]
        public static bool IsValidChar(char c)
        {
            //from AnitXss library
            return
                (((c > '`') && (c < '{')) || ((c > '@') && (c < '['))) ||
                (((c > '/') && (c < ':')) || (((c == '.') ||
                (c == '-')) || (c == '_')));
        }

        public static UrlPath Absolute(params string[] segments)
        {
            Contract.Requires<ArgumentNullException>(segments != null);
            Contract.Ensures(Contract.Result<UrlPath>() != null);

            return new UrlPath(segments, UrlPathOptions.Absolute);
        }

        public static UrlPath Relative(params string[] segments)
        {
            Contract.Requires<ArgumentNullException>(segments != null);
            Contract.Ensures(Contract.Result<UrlPath>() != null);

            return new UrlPath(segments, UrlPathOptions.None);
        }

        public static UrlPath AppRelative(params string[] segments)
        {
            Contract.Requires<ArgumentNullException>(segments != null);
            Contract.Ensures(Contract.Result<UrlPath>() != null);

            return new UrlPath(new[] { "~" }.Concat(segments), UrlPathOptions.None);
        }

        public static UrlPath Parse(string path)
        {
            Contract.Requires<ArgumentNullException>(path != null);
            Contract.Ensures(Contract.Result<UrlPath>() != null);

            bool isAbsolute = path.StartsWith("/", StringComparison.Ordinal);

            var pathOptions = isAbsolute ? UrlPathOptions.Absolute : UrlPathOptions.None;

            bool hasTrailingSlashes = path.EndsWith("/", StringComparison.Ordinal);
            if (hasTrailingSlashes)
            {
                pathOptions |= UrlPathOptions.HasTrailingSlash;
            }

            var trimmedPath = isAbsolute && path.Length > 0 ? path.Substring(1) : path;
            var segments1 = trimmedPath
                .Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)
                .Select(Url.Decode);

            return new UrlPath(segments1, pathOptions);
        }

        private readonly IEnumerable<string> segments;
        public IEnumerable<string> Segments
        {
            get
            {
                Contract.Ensures(Contract.Result<IEnumerable<string>>() != null);
                return this.segments;
            }
        }

        public bool IsAbsolute
        {
            get { return (options & UrlPathOptions.Absolute) == UrlPathOptions.Absolute; }
        }

        public static readonly UrlPath Empty = new UrlPath(Enumerable.Empty<string>(), UrlPathOptions.None);
        public static readonly UrlPath Root = new UrlPath(Enumerable.Empty<string>(), UrlPathOptions.Absolute);
        public const char PathSeparator = '/';

        public UrlPath(IEnumerable<string> segments, UrlPathOptions options)
        {
            Contract.Requires<ArgumentNullException>(segments != null);

            this.options = options;

            this.segments = segments.ToList();
        }

        public UrlPath(IEnumerable<string> segments, bool isAbsolute)
            : this(segments, isAbsolute ? UrlPathOptions.Absolute : UrlPathOptions.None)
        {
        }

        public UrlPath Combine(string path)
        {
            Contract.Requires<ArgumentNullException>(String.IsNullOrEmpty(path) == false);
            Contract.Ensures(Contract.Result<UrlPath>() != null);

            return Combine(Parse(path));
        }

        public RelativeUrl ToUrl()
        {
            return RelativeUrl.FromPath(this);
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
        public string GetFileExtension()
        {
            var lastSegment = this.Segments.LastOrDefault();
            if (lastSegment == null)
                return String.Empty;

            return Path.GetExtension(lastSegment);
        }

        public UrlPath Combine(UrlPath path)
        {
            Contract.Requires<ArgumentNullException>(path != null);
            Contract.Ensures(Contract.Result<UrlPath>() != null);

            if (path.IsAbsolute)
                return path;

            var combinedOptions = UrlPathOptions.None;
            if (this.IsAbsolute)
            {
                combinedOptions |= UrlPathOptions.Absolute;
            }
            if (path.HasTrailingSlash)
            {
                combinedOptions |= UrlPathOptions.HasTrailingSlash;
            }

            return new UrlPath(this.Segments.Concat(path.Segments), combinedOptions);
        }

        public bool HasTrailingSlash
        {
            get { return (options & UrlPathOptions.HasTrailingSlash) == UrlPathOptions.HasTrailingSlash; }
        }

        public override string ToString()
        {
            var encodedSegments = new List<string>();

            bool first = true;
            foreach (var segment in this.Segments)
            {
                bool startsWithTilde = first && this.IsAbsolute == false && segment == "~";
                bool isDriveIndicator = first && segment.Length == 2 && segment[1] == ':';

                if (startsWithTilde || isDriveIndicator)
                    encodedSegments.Add(segment);
                else
                    encodedSegments.Add(Url.Encode(segment));

                first = false;
            }

            var path = encodedSegments.JoinStrings("/");
            if (HasTrailingSlash && !path.EndsWith("/", StringComparison.Ordinal) && this.segments.Any())
            {
                path = path + "/";
            }

            if (this.IsAbsolute)
                return "/" + path;

            return path;
        }

        public static bool IsValid(string path)
        {
            Contract.Requires<ArgumentNullException>(path != null);

            foreach (var ch in path)
            {
                if (ch != PathSeparator && IsValidChar(ch) == false)
                    return false;
            }
            return true;
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Unescaped")]
        public string ToUnescapedString()
        {
            Contract.Ensures(Contract.Result<string>() != null);

            var path = this.Segments.Select(s => s.Replace("/", "%2f")).JoinStrings("/");
            if (this.IsAbsolute)
                return "/" + path;
            return path;
        }

        public bool Equals(UrlPath other)
        {
            if (ReferenceEquals(null, other)) return false;
            if (ReferenceEquals(this, other)) return true;
            return this.ToString() == other.ToString();
        }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            if (ReferenceEquals(this, obj)) return true;
            if (obj.GetType() != typeof(UrlPath)) return false;
            return Equals((UrlPath)obj);
        }

        public override int GetHashCode()
        {
            return this.ToString().GetHashCode();
        }

        public static bool operator ==(UrlPath left, UrlPath right)
        {
            return Equals(left, right);
        }

        public static bool operator !=(UrlPath left, UrlPath right)
        {
            return !Equals(left, right);
        }
    }
}
